/*******************************************************************************
*                                                                              *
*                               Kyoto University                               *
*                               Okuno laboratory                               *
*                                    Tabulæ                                    *
*                          Louis-Kenzo Furuya Cahier                           *
*                                                                              *
********************************************************************************
*                                                                              *
* Copyright (c) 2012 Louis-Kenzo Furuya Cahier                                 *
*                                                                              *
* Permission is hereby granted, free of charge, to any person obtaining a copy *
* of this software and associated documentation files (the "Software"), to     *
* deal in the Software without restriction, including without limitation the   *
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or  *
* sell copies of the Software, and to permit persons to whom the Software is   *
* furnished to do so, subject to the following conditions:                     *
*                                                                              *
* The above copyright notice and this permission notice shall be included in   *
* all copies or substantial portions of the Software.                          *
*                                                                              *
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR   *
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,     *
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE  *
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER       *
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING      *
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS *
* IN THE SOFTWARE.                                                             *
*                                                                              *
*******************************************************************************/

/********************************** Includes **********************************/
// Standard library
#include <string>
#include <iostream>
#include <iomanip>
#include <cmath>
// Boost
#include "boost/date_time/posix_time/posix_time.hpp"
#include "boost/thread.hpp"
#include "boost/thread/mutex.hpp"
// Project
#include "Logger.h"

using namespace std;
using namespace boost;

const char* bronze  = "\033[00;30m";
const char* yellow  = "\033[00;31m";
const char* green   = "\033[00;32m";
const char* orange  = "\033[00;33m";
const char* violet  = "\033[00;34m";
const char* fuchsia = "\033[00;35m";
const char* blue    = "\033[00;36m";
const char* white   = "\033[00;37m";

const char* bold_bronze  = "\033[01;30m";
const char* bold_yellow  = "\033[01;31m";
const char* bold_green   = "\033[01;32m";
const char* bold_orange  = "\033[01;33m";
const char* bold_violet  = "\033[01;34m";
const char* bold_fuchsia = "\033[01;35m";
const char* bold_blue    = "\033[01;36m";
const char* bold_white   = "\033[01;37m";

/************************************ Main ************************************/
namespace Tabulae {
	//====================> Class variables (public)
	TimeStamp TimeStamp::Now  = TimeStamp::now();
	
	//====================> Constructors
	TimeStamp::TimeStamp()
	: _time() 
	{
		
	}
	
	TimeStamp::TimeStamp(const boost::posix_time::ptime& time)
	: _time(time) 
	{
		
	}
	
	//====================> Class methods
	TimeStamp TimeStamp::now() {
		return TimeStamp(posix_time::microsec_clock::local_time());
	}
	
	//====================> Public methods
	boost::posix_time::ptime TimeStamp::time() const {
		return _time;
	}
	
	//====================> Constructors
	Location::Location()
	: _location() {
		
	}
	
	Location::Location(const std::string& location)
	: _location(location)
	{
		
	}
	
	//====================> Public methods
	const std::string& Location::string() const {
		return _location;
	}
	
	//====================> Class properties (private)
	LoggingLevel Logger::minimum_logging_level = Tabulae::Trace;
	boost::mutex Logger::access_mutex;
	boost::mutex Logger::line_printing_mutex;
	bool Logger::line_printing = false;
	
	//====================> Class helpers (private)
	void Logger::lock_line_printing() {
		if (!line_printing) {
			line_printing_mutex.lock();
			line_printing = true;
		}
	}
	
	void Logger::unlock_line_printing() {
		if (line_printing) {
			line_printing_mutex.unlock();
			line_printing = false;
		}
	}
	
	//====================> Class variables (public)
	Logger Logger::Trace   = Logger(Tabulae::Trace);
	Logger Logger::Debug   = Logger(Tabulae::Debug);
	Logger Logger::Info    = Logger(Tabulae::Info);
	Logger Logger::Warning = Logger(Tabulae::Warning);
	Logger Logger::Error   = Logger(Tabulae::Error);
	Logger Logger::Fatal   = Logger(Tabulae::Fatal);
	
	
	//====================> Class methods
	void Logger::log_level(LoggingLevel level) {
		minimum_logging_level = level;
	}
	
	//====================> Constructors
	Logger::Logger(LoggingLevel level) 
	: _level(level),
	  _internal_stream(),
	  _external_stream(0),
	  _auto_timestamp(true),
	  _relative_timestamp(false),
	  _auto_timestamp_inhibited(false),
	  _last_timestamp(posix_time::microsec_clock::local_time()),
	  _last_elapsed(0,0,0,0),
	  _last_location(),
	  _last_thread_id()
	{
		switch(level) {
			case Tabulae::Trace:
				set_output(std::cout);
				break;
			case Tabulae::Debug:
				set_output(std::cout);
				break;
			case Tabulae::Info:
				set_output(std::cout);
				break;
			case Tabulae::Warning:
				set_output(std::cerr);
				break;
			case Tabulae::Error:
				set_output(std::cerr);
				break;
			case Tabulae::Fatal:
				set_output(std::cerr);
				break;
		}
	}
	
	Logger::Logger(const Logger& other_logger)
	: _level(other_logger._level),
	  _internal_stream(other_logger._internal_stream),
	  _external_stream(other_logger._external_stream),
	  _auto_timestamp(other_logger._auto_timestamp),
	  _relative_timestamp(other_logger._relative_timestamp),
	  _auto_timestamp_inhibited(other_logger._auto_timestamp_inhibited),
	  _last_timestamp(other_logger._last_timestamp),
	  _last_elapsed(other_logger._last_elapsed),
	  _last_location(other_logger._last_location),
	  _last_thread_id(other_logger._last_thread_id)
	{
		
	}
	
	//====================> Public methods
	template <typename T>
	Logger& Logger::operator<<(T input) {
		access_mutex.lock();
		if (should_log()) {
			lock_line_printing();
			print_auto_timestamp_if_necessary();
			stream() << input;
		}
		access_mutex.unlock();
		
		return *this;
	}
	
	template <typename T>
	Logger& Logger::operator<<(T* input) {
		access_mutex.lock();
		if (should_log()) {
			lock_line_printing();
			print_auto_timestamp_if_necessary();
			stream() << input;
		}
		access_mutex.unlock();
		
		return *this;
	}
	
	Logger& Logger::operator<<(std::ostream& (*input)(std::ostream&)) {
		access_mutex.lock();
		if (should_log()) {
			if (input == static_cast<std::ostream& (*)(std::ostream&)>(std::endl)) {
				print_auto_timestamp_if_necessary();
				_auto_timestamp_inhibited = false;
				
				stream() << input;
				unlock_line_printing();
			} else {
				stream() << input;
			}
		}
		access_mutex.unlock();
		
		return *this;
	}
	
	Logger& Logger::operator<<(std::ios& (*input)(std::ios&)) {
		access_mutex.lock();
		if (should_log()) {
			lock_line_printing();
			stream() << input;
		}
		access_mutex.unlock();
		
		return *this;
	}
	
	Logger& Logger::operator<<(std::ios_base& (*input)(std::ios_base&)) {
		access_mutex.lock();
		if (should_log()) {
			lock_line_printing();
			stream() << input;
		}
		access_mutex.unlock();
		
		return *this;
	}
	
	Logger& Logger::operator<<(const TimeStamp& timestamp) {
		access_mutex.lock();
		if (should_log()) {
			lock_line_printing();
			// Inhibit the auto timestamping if a timestamp is manually requested on an inhibited (typically beginning of line) logger
			if (!_auto_timestamp_inhibited) _auto_timestamp_inhibited = true;
			
			print_timestamp_if_necessary(timestamp);
		}
		access_mutex.unlock();
		
		return *this;
	}
	
	Logger& Logger::operator<<(const Location& location) {
		access_mutex.lock();
		if (should_log()) {
			lock_line_printing();
			print_auto_timestamp_if_necessary();
			if (color_support()) {
				// Set colors
				const char* location_color = (location.string()!=_last_location ? bold_orange : bronze);
				stream()  << location_color << location.string()
				          << bold_bronze << " > " 
				          << white;
				
				// Save current location
				_last_location = location.string();
			} else {
				stream() << location.string() << " ⌖ ";
			}
		}
		access_mutex.unlock();
		
		return *this;
	}
	
	void Logger::set_output(std::ostream& external_stream) {
		destroy_internal_stream_reference_if_necessary();
		
		_external_stream = &external_stream;
	}
	
	void Logger::set_output(const std::string& path) {
		destroy_internal_stream_reference_if_necessary();
		
		_internal_stream.reset(new ofstream(path.c_str()));
	}
	
	void Logger::set_output(const char* path) {
		destroy_internal_stream_reference_if_necessary();
		
		_internal_stream.reset(new ofstream(path));
	}
	
	void Logger::auto_timestamp(bool auto_timestamp) {
		_auto_timestamp = auto_timestamp;
	}
	
	void Logger::relative_timestamp(bool relative_timestamp) {
		_relative_timestamp = relative_timestamp;
	}
	
	//====================> Helpers
	bool Logger::is_internal_stream() const {
		return _internal_stream; // Implicit conversion to bool for shared_ptr returns emptiness
	}
	
	bool Logger::is_external_stream() const {
		return !is_internal_stream();
	}
	
	std::ostream& Logger::stream() {
		if (is_internal_stream()) {
			return *_internal_stream.get();
		} else {
			return *_external_stream;
		}
	}
	
	void Logger::destroy_internal_stream_reference_if_necessary() {
		// Destroy internal reference to stream if necessary; external_streams are not owned so do not need to be deallocated
		if (is_internal_stream()) _internal_stream.reset();
	}
	
	bool Logger::has_stream() const {
		if (is_internal_stream()) {
			return true;
		} else {
			return _external_stream!=0;
		}
	}
	
	bool Logger::should_log() const {
		return _level >= Logger::minimum_logging_level 
		    && has_stream();
	}
	
	void Logger::print_timestamp_if_necessary(const TimeStamp& timestamp, bool auto_timestamp) {
		if (!should_log()) return;
		
		// If a timestamp is not provided, obtain one now
		boost::posix_time::ptime print_timestamp;
		if (timestamp.time().is_not_a_date_time()) {
			print_timestamp = posix_time::microsec_clock::local_time();
		} else {
			print_timestamp = timestamp.time();
		}
		boost::posix_time::time_duration elapsed = print_timestamp - Logging::_start_time;
		
		// Find and print the timestamp
		if (_relative_timestamp) {
			if (color_support() && auto_timestamp) {
				// Extract moment information
				std::ostringstream hour;
				std::ostringstream minute;
				std::ostringstream second;
				std::ostringstream fractional_second;
				
				boost::posix_time::time_facet* hour_format              = new boost::posix_time::time_facet();
				boost::posix_time::time_facet* minute_format            = new boost::posix_time::time_facet();
				boost::posix_time::time_facet* second_format            = new boost::posix_time::time_facet();
				boost::posix_time::time_facet* fractional_second_format = new boost::posix_time::time_facet();
				             hour_format->time_duration_format("%O");
				           minute_format->time_duration_format("%M");
				           second_format->time_duration_format("%S");
				fractional_second_format->time_duration_format("%f");
				
				hour             .imbue(std::locale(std::cout.getloc(), hour_format));
				minute           .imbue(std::locale(std::cout.getloc(), minute_format));
				second           .imbue(std::locale(std::cout.getloc(), second_format));
				fractional_second.imbue(std::locale(std::cout.getloc(), fractional_second_format));
				
				hour              << elapsed;
				minute            << elapsed;
				second            << elapsed;
				fractional_second << elapsed;
				
				// Set colors
				const char* hour_color              = (elapsed.hours()  !=_last_elapsed.hours()   ? bold_green : bronze);
				const char* minute_color            = (elapsed.minutes()!=_last_elapsed.minutes() ? bold_green : bronze);
				const char* second_color            = (elapsed.seconds()!=_last_elapsed.seconds() ? bold_green : bronze);
				const char* fractional_second_color = bold_green;
				const char* auto_marker_color       = (boost::this_thread::get_id()!=_last_thread_id ? bold_fuchsia : bold_bronze);
				
				stream() << hour_color              << hour.str()   << ":"
				         << minute_color            << minute.str() << ":"
				         << second_color            << second.str() << ","
				         << fractional_second_color << fractional_second.str()
				         << auto_marker_color       << " > "
				         << white 
				         << flush;
			} else {
				stream() << boost::posix_time::to_simple_string(elapsed) << " > " << flush;
			}
		} else {
			if (color_support() && auto_timestamp) {
				// Extract moment information
				std::ostringstream year;
				std::ostringstream month;
				std::ostringstream day;
				std::ostringstream hour;
				std::ostringstream minute;
				std::ostringstream second;
				std::ostringstream fractional_second;
				boost::posix_time::time_facet* year_format   = new boost::posix_time::time_facet("%Y");
				boost::posix_time::time_facet* month_format  = new boost::posix_time::time_facet("%m");
				boost::posix_time::time_facet* day_format    = new boost::posix_time::time_facet("%d");
				boost::posix_time::time_facet* hour_format   = new boost::posix_time::time_facet("%H");
				boost::posix_time::time_facet* minute_format = new boost::posix_time::time_facet("%M");
				boost::posix_time::time_facet* second_format = new boost::posix_time::time_facet("%S");
				boost::posix_time::time_facet* fractional_second_format = new boost::posix_time::time_facet("%f");
				year             .imbue(std::locale(std::cout.getloc(), year_format));
				month            .imbue(std::locale(std::cout.getloc(), month_format));
				day              .imbue(std::locale(std::cout.getloc(), day_format));
				hour             .imbue(std::locale(std::cout.getloc(), hour_format));
				minute           .imbue(std::locale(std::cout.getloc(), minute_format));
				second           .imbue(std::locale(std::cout.getloc(), second_format));
				fractional_second.imbue(std::locale(std::cout.getloc(), fractional_second_format));
				
				year              << print_timestamp;
				month             << print_timestamp;
				day               << print_timestamp;
				hour              << print_timestamp;
				minute            << print_timestamp;
				second            << print_timestamp;
				fractional_second << print_timestamp;
				
				// Check if we're in the same thread as last time we printed an automatic block indicator
				bool different_thread = boost::this_thread::get_id()!=_last_thread_id;
				
				// Set colors
				const char* year_color              = (print_timestamp.date().year() !=_last_timestamp.date().year()  ? yellow : bronze);
				const char* month_color             = (print_timestamp.date().month()!=_last_timestamp.date().month() ? yellow : bronze);
				const char* day_color               = (print_timestamp.date().day()  !=_last_timestamp.date().day()   ? yellow : bronze);
				const char* hour_color              = (print_timestamp.time_of_day().hours()  !=_last_timestamp.time_of_day().hours()   ? bold_green : bold_bronze);
				const char* minute_color            = (print_timestamp.time_of_day().minutes()!=_last_timestamp.time_of_day().minutes() ? bold_green : bold_bronze);
				const char* second_color            = (print_timestamp.time_of_day().seconds()!=_last_timestamp.time_of_day().seconds() ? bold_green : bold_bronze);
				const char* fractional_second_color = bold_green;
				const char* auto_marker_color       = (different_thread ? bold_fuchsia : bronze);
				
				// Print
				stream() << year_color              << year.str()              << "-"
				         << month_color             << month.str()             << "-"
				         << day_color               << day.str()               << " ⌚ "
				         << hour_color              << hour.str()              << ":"
				         << minute_color            << minute.str()            << ":"
				         << second_color            << second.str()            << ","
				         << fractional_second_color << fractional_second.str()
				         << auto_marker_color       << (different_thread ? " ⎇  " : " ⌇  ") << std::setw(14) << std::left << boost::this_thread::get_id() << " "
				         << white
				         << flush;
			} else {
				stream() << boost::posix_time::to_simple_string(print_timestamp) << " > " << flush;
			}
		}
		
		// Save current timestamp
		_last_timestamp = print_timestamp;
		_last_elapsed   = elapsed;
		_last_thread_id = boost::this_thread::get_id();
	}
	
	void Logger::print_auto_timestamp_if_necessary() {
		if ( _auto_timestamp_inhibited || !_auto_timestamp ) {
			return;
		} else {
			print_timestamp_if_necessary(TimeStamp(), true);
			
			if (!_auto_timestamp_inhibited) _auto_timestamp_inhibited = true;
		}
	}
	
	bool Logger::color_support() const {
		#ifdef __unix__
		return (is_external_stream() && (_external_stream == &std::cout || 
		                                 _external_stream == &std::cerr ||
		                                 _external_stream == &std::clog));
		#else
		return false;
		#endif 
	}
}

/*************************** Template instantiations **************************/
namespace Tabulae {
	template Logger& Logger::operator<<(int           input);
	template Logger& Logger::operator<<(unsigned int  input);
	template Logger& Logger::operator<<(unsigned long input);
	template Logger& Logger::operator<<(float         input);
	template Logger& Logger::operator<<(double        input);
	template Logger& Logger::operator<<(long          input);
	template Logger& Logger::operator<<(char const*   input);
	template Logger& Logger::operator<<(std::string   input);
}

/******************************************************************************/

